package com.tchepannou.limbe.gae.dao;

import com.google.appengine.api.datastore.*;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.tchepannou.limbe.annotation.Cache;
import com.tchepannou.limbe.annotation.Column;
import com.tchepannou.limbe.annotation.Indexed;
import com.tchepannou.limbe.annotation.Id;
import com.tchepannou.limbe.dao.Dao;
import com.tchepannou.limbe.gae.converter.KeyConverter;
import com.tchepannou.limbe.helper.CamelCaseHelper;
import com.tchepannou.limbe.model.Model;
import com.tchepannou.util.StringUtil;
import com.tchepannou.util.converter.Converter;
import com.tchepannou.util.converter.ConverterFactory;
import com.tchepannou.util.converter.DefaultConverterFactory;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

public abstract class AbstractGaeDao
    implements Dao
{
    //-- Static
    public static final String FIELD_ID = "id";
    public static final int MAX_LIMIT = 1000;
    private static final Converter KEY_CONVERTER = new KeyConverter();
    private static final Map<Class, Field> ID_FIELD = new HashMap<Class, Field>();
    private static final Map<Class, Set<Field>> PERSISTENT_FIELDS = new HashMap<Class, Set<Field>>();


    //-- Dao overrides
    public Model findById (Serializable id)
    {
        if (id instanceof Key)
        {
            return findByKey ((Key)id);
        }
        else if (id instanceof String)
        {
            Key key = toKey (id.toString());
            return key != null ? findByKey (key) : null;
        }

        throw new IllegalArgumentException("Invalid ID: " + id);
    }

    public Map<String, Model> findByIds(Collection<String> ids)
    {
        Set<Key> keys = new HashSet<Key>();
        Map<Key, String> idMap = new HashMap<Key, String>();
        for (String id : ids)
        {
            Key key = toKey(id);
            idMap.put(key, id);
            if (key != null)
            {
                keys.add(key);
            }
        }

        Map<Key, Model> models = findByKeys(keys);
        Map<String, Model> result = new HashMap<String, Model>();
        for (Key key : models.keySet())
        {
            String id = idMap.get(key);
            if (id != null)
            {
                result.put(id, models.get(key));
            }
        }
        return result;
    }


    //-- Abstract method
    protected abstract Class<? extends Model> getModelClass();


    //-- Public methods
    public final void save (Model model)
    {
        if (model.getId() == null)
        {
            create(model);
        }
        else
        {
            update(model);
        }
    }

    public final void save(Collection<? extends Model> models)
    {
        /* Model -> Entity */
        DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
        List<Entity> entities = new ArrayList<Entity>();
        Map<Model, Entity> map = new HashMap<Model, Entity>();
        for (Model model : models)
        {
            try
            {
                if (!model.getClass().equals(getModelClass()))
                {
                    throw new IllegalArgumentException("Expecting model of type <" + getModelClass() + ">. Got <" + model.getClass() + ">");
                }

                Entity entity = null;
                if (model.getId() == null)
                {
                    Key parent = createParentKey(model);
                    String kind = getKind(model.getClass());
                    entity = parent == null ? new Entity(kind) : new Entity(kind, parent);
                }
                else
                {
                    Key key = toKey(model);
                    entity = ds.get(key);
                }

                toEntity(model, entity);
                entities.add(entity);
                map.put(model, entity);
            }
            catch (EntityNotFoundException e)
            {

            }
        }

        /* Save */
        ds.put(entities);

        /* Set ids */
        for (Model model : models)
        {
            if (model.getId() == null)
            {
                Entity entity = map.get(model);
                if (entity != null)
                {
                    setIdField(entity, model);
                }
            }
        }

        /* remove from cache */
        Cache cache = getModelClass().getAnnotation(Cache.class);
        if (cache != null)
        {
            MemcacheService ms = getMemcacheService();
            List<Key> keys = new ArrayList<Key> ();
            for (Entity entity : entities)
            {
                keys.add(entity.getKey());
            }
            ms.deleteAll(keys);
        }
    }

    public Key create (Model model)
    {
        if (!model.getClass().equals(getModelClass()))
        {
            throw new IllegalArgumentException("Expecting model of type <" + getModelClass() + ">. Got <" + model.getClass() + ">");
        }

        model.onSave();

        /* create */
        Key parent = createParentKey(model);
        String kind = getKind(model.getClass());
        Entity entity = parent == null ? new Entity(kind) : new Entity(kind, parent);
        toEntity(model, entity);

        /* Store */
        DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
        ds.put(entity);

        /* Set the ID */
        setIdField(entity, model);
        return entity.getKey();
    }

    public Key update (Model model)
    {
        if (!model.getClass().equals(getModelClass()))
        {
            throw new IllegalArgumentException("Expecting model of type <" + getModelClass() + ">. Got <" + model.getClass() + ">");
        }

        DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
        Key key = toKey(model);
        try
        {
            model.onSave();

            /* update */
            Entity entity = ds.get(key);
            toEntity(model, entity);
            ds.put(entity);

            /* remove from cache */
            Cache cache = model.getClass().getAnnotation(Cache.class);
            if (cache != null)
            {
                MemcacheService ms = getMemcacheService();
                ms.delete(key);
            }

            return key;
        }
        catch (EntityNotFoundException e)
        {
            return null;
        }
    }

    public final void delete (String id)
    {
        Key key = toKey(id);
        delete(key);
    }

    public void delete(Key key)
    {
        if (key == null)
        {
            return;
        }

        try
        {
            /* delete */
            DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
            ds.delete(key);

            /* remove from cache */
            Cache cacheable = getModelClass().getAnnotation(Cache.class);
            if (cacheable != null)
            {
                MemcacheService ms = getMemcacheService();
                ms.delete(key);
            }
        }
        catch (IllegalArgumentException e)
        {
        }
    }

    public Model findByKey (Key key)
    {
        /* check the key */
        if (key == null)
        {
            return null;
        }

        /* check the getKind */
        Class<? extends Model> clazz = getModelClass();
        String kind = getKind(clazz);
        if (!kind.equalsIgnoreCase (key.getKind ()) )
        {
            return null;
        }

        /* get from cache */
        Cache cache = clazz.getAnnotation(Cache.class);
        if (cache != null)
        {
            MemcacheService ms = getMemcacheService();
            Model model = (Model)ms.get(key);
            if (model != null)
            {
                getLogger().log(Level.FINE, "Found in cache. " + key + "=" + model);
                return model;
            }
            else
            {
                getLogger().log(Level.FINE, "Not found in cache. " + key + "=null");
            }
        }

        /* get from datastore */
        DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
        try
        {
            Entity entity = ds.get(key);
            if (entity != null)
            {
                getLogger().log(Level.FINE, "Found in datastore. " + key + "=" + entity);
                Model model = clazz.newInstance();
                toModel (entity, model);
                model.onLoad();

                /* store in cache */
                if (cache != null)
                {
                    getMemcacheService().put(entity.getKey(), model);
                }
                return model;
            }
            else
            {
                getLogger().log(Level.FINE, "not Found in datastore. " + key + "=null");
                return null;
            }
        }
        catch (EntityNotFoundException e)
        {
            return null;
        }
        catch (InstantiationException e)
        {
            throw new IllegalStateException("Unable to instantiate : " + clazz, e);
        }
        catch (IllegalAccessException e)
        {
            throw new IllegalStateException("Unable to instantiate : " + clazz, e);
        }
        catch (IllegalArgumentException e)
        {
            return null;
        }
    }

    public Map<Key, Model> findByKeys (Collection<Key> keys)
    {
        Map<Key, Model> models = new HashMap<Key, Model>();
        for (Key key : keys)
        {
            Model model = findByKey(key);
            if (model != null)
            {
                models.put(key, model);
            }
        }
        return models;
    }


    //-- Protected methods
    protected Logger getLogger ()
    {
        return Logger.getLogger(getClass().getName());
    }

    protected final void toEntity(Model model, Entity entity)
    {
        ConverterFactory cf = DefaultConverterFactory.getInstance();
        for (Field field : getPersistentFields(model.getClass()))
        {
            String name = getColumnName(field);
            Class type = getColumnType(field);
            try
            {
                Object value = field.get(model);
                Converter converter = cf.getConverter(type);
                if (converter != null)
                {
                    value = converter.convert(value);
                }

                if (isIndexed(field))
                {
                    entity.setProperty(name, value);
                }
                else
                {
                    entity.setUnindexedProperty(name, value);
                }
            }
            catch (IllegalAccessException e)
            {
                throw new IllegalStateException("Unable to get " + field.getName() + ":" + type.getName(), e);
            }
        }
    }

    protected final void toModel(Entity entity, Model model)
    {
        ConverterFactory cf = DefaultConverterFactory.getInstance();

        /* Set persistent fields */
        Class clazz = model.getClass();
        for (Field field : getPersistentFields(clazz))
        {
            String name = getColumnName(field);
            Object value = entity.getProperty(name);
            setField(model, field, value);
        }

        /* Set ID field */
        Key key = entity.getKey();
        Field id = getIdField(clazz);
        setField(model, id, key);

        /* Set parent field */
        Field parent = getParentField(clazz);
        if (parent != null)
        {
            Key value = key.getParent();
            setField(model, parent, key.getParent());
        }
    }

    protected final String getKind(Class<?> clazz)
    {
        return getModelClass().getSimpleName();
    }

    protected final Key toKey (String key)
    {
        return (Key)KEY_CONVERTER.convert(key);
    }

    protected final Key toKey (Model model)
    {
        if (model == null)
        {
            return null;
        }
        else
        {
            Serializable id = model.getId  ();
            return id != null ? toKey(id.toString ()): null;
        }
    }

    protected final Key createParentKey (Model model)
    {
        Class clazz = model.getClass();
        Field field = getParentField(clazz);
        if (field != null)
        {
            try
            {
                Object value = field.get(model);
                if (value != null)
                {
                    return toKey(value.toString());
                }
            }
            catch (IllegalAccessException e)
            {
                throw new IllegalStateException("Unable to get value from " + clazz.getName() + "->" + field.getName(), e);
            }
        }
        return null;
    }

    protected final <T extends Model> T asSingleton (Query q, Class<T> type)
    {
        List<T> result = asList(q, 1, 0, type);
        return !result.isEmpty() ? result.get(0) : null;
    }

    protected final <T extends Model> List<T> asList (Query q, Class<T> type)
    {
        return asList(q, MAX_LIMIT, 0, type);
    }

    protected final <T extends Model> List<T> asList (Query q, int limit, int offset, Class<T> type)
    {
        FetchOptions fo = FetchOptions.Builder.withLimit(limit).offset(offset);
        List<T> result = new PersistentList<T>(q, this, fo);
        return result;
    }

    protected final int count (Query q)
    {
        int count = 0;
        int limit = MAX_LIMIT;
        DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
        for (int offset = 0; ;offset+=limit)
        {
            FetchOptions fo = FetchOptions.Builder.withLimit(limit).offset(offset);
            q.setKeysOnly();
            PreparedQuery pq = ds.prepare(q);
            int tmp = pq.countEntities(fo);
            count += tmp;

            if (tmp < limit)
            {
                break;
            }
        }
        return count;
    }

    protected final Query createQuery ()
    {
        Class clazz = getModelClass();
        String kind = getKind(clazz);
        Query q = new Query (kind);
        return q;
    }

    protected final Query createQueryWithAccessor (String accessorId)
    {
        Class clazz = getModelClass();
        String kind = getKind(clazz);
        Key key = toKey(accessorId);
        Query q = new Query (kind);
        q.setAncestor(key);
        return q;
    }

    //-- Private
    private boolean isCacheabled ()
    {
        return getModelClass().getAnnotation(Cache.class) != null;
    }
    private String getColumnName (Field field)
    {
        Column column = field.getAnnotation(Column.class);
        String name = column != null ? column.name() : null;

        return StringUtil.isEmpty(name) ?  CamelCaseHelper.toUnderscore(field.getName()) : name;
    }

    private Class getColumnType (Field field)
    {
        Column column = field.getAnnotation(Column.class);
        if (column != null)
        {
            Column.Type type = column.type();
            if (Column.Type.DEFAULT.equals(type))
            {
                return field.getType();
            }
            else if (Column.Type.EMAIL.equals(type))
            {
                return Email.class;
            }
            else if (Column.Type.URL.equals(type))
            {
                return Link.class;
            }
            else if (Column.Type.TEXT.equals(type))
            {
                return Text.class;
            }
            else
            {
                throw new IllegalStateException("No class associated with " + type);
            }
        }
        return field.getType();
    }

    private boolean isIndexed(Field field)
    {
        return field.getAnnotation(Indexed.class) != null;
    }

    private void setIdField(Entity entity, Model model)
    {
        Field field = getIdField(model.getClass());
        setField(model, field, entity.getKey());
    }

    private void setField (Model model, Field field, Object value)
    {
        Object xvalue;
        Class type = field.getType();
        ConverterFactory cf = DefaultConverterFactory.getInstance();
        Converter converter = cf.getConverter(type);
        if (converter != null)
        {
            xvalue = converter.convert(value);
        }
        else
        {
            xvalue = value;
        }

        try
        {
            field.set(model, xvalue);
        }
        catch (IllegalAccessException e)
        {
            throw new IllegalStateException("Unable to set " + field.getName() + ":" + type.getName() + " = " + xvalue, e);
        }
    }

    private Field getIdField (Class clazz)
    {
        Field id = ID_FIELD.get(clazz);
        if (id == null)
        {
            for (Field field : clazz.getFields())
            {
                if (field.getAnnotation(Id.class) != null)
                {
                    id = field;
                }
            }

            if (id == null)
            {
                try
                {
                    id = clazz.getField(FIELD_ID);
                }
                catch (NoSuchFieldException e)
                {
                    throw new IllegalStateException(clazz.getName() + " has no ID field", e);
                }
            }

            synchronized (ID_FIELD)
            {
                ID_FIELD.put(clazz, id);
            }
        }
        return id;
    }

    private Field getParentField (Class clazz)
    {
        Field pk = getIdField(clazz);
        if (pk != null)
        {
            Id id = pk.getAnnotation(Id.class);
            String parent = id.parent();
            if (!StringUtil.isEmpty(parent))
            {
                try
                {
                    return clazz.getField(parent);
                }
                catch (NoSuchFieldException e)
                {
                    throw new IllegalStateException("@Id.parent doesn't match any field in " + clazz.getName() + ": " + parent, e);
                }
            }
        }
        return null;
    }

    private Set<Field> getPersistentFields (Class clazz)
    {
        Set<Field> fields = PERSISTENT_FIELDS.get(clazz);
        if (fields == null)
        {
            Field pk = getIdField(clazz);
            String parent = pk.getAnnotation(Id.class).parent();

            fields = new HashSet<Field>();
            for (Field field : clazz.getFields())
            {
                int modifier = field.getModifiers();
                if (Modifier.isStatic(modifier) || Modifier.isTransient(modifier))
                {
                    continue;
                }
                else if (parent.equals(field.getName()))
                {
                    continue;
                }
                else if (field.equals(pk))
                {
                    continue;
                }
                else
                {
                    fields.add(field);
                }
            }

            synchronized (PERSISTENT_FIELDS)
            {
                PERSISTENT_FIELDS.put(clazz, fields);
            }
        }
        return fields;
    }

    private MemcacheService getMemcacheService()
    {
        Class clazz = getModelClass();
        Cache cache = (Cache)clazz.getAnnotation(Cache.class);
        if (cache != null)
        {
            String region = cache.region();
            if (region.length() == 0)
            {
                region = getKind(clazz);
            }
            return MemcacheServiceFactory.getMemcacheService(region);
        }
        else
        {
            throw new IllegalStateException("Caching not supported");
        }
    }
}
